﻿using EricOulashin;
using Lung_Sounds_V2;
using Microsoft.Win32;
using NAudio.Wave;
using pHMb.Spectrogram;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace LungSoundWizard
{
    public class AudioProcessor
    {
        WaveIn _waveInStream;
        WaveOut _waveOutStream;

        private byte[] _recorderBuffer;
        private short[] _waveBuffer;
        private byte[] _playerBuffer;
        private bool _isPaused;
        private WAVFile _wavSaver;
        private WAVFile _wavOpener;
        private string _wavLocation;

        private bool _processGraphs = false;

        private bool _enableFiltering = false;

        private FifoStream _playerStream;
        private int _sampleRate;
        private int _bitsPerSample;
        private int _samplesPerWindow;
        private double _maxAmplitude;
        private double _amplification = 1;

        private DateTime _lastCaptureTime;
        private int _lostTime;

        private int _limitPercent = 100;

        // The size of the audio in buffer = this * WindowsSize
        private int _bufferSizeMultiplier;

        private SpectrogramPane _spectrogramPane;

        public pHMb.Spectrogram.SpectrogramPane Spectrogram { get { return _spectrogramPane; } }

        public bool isSpectrogramLog
        {
            get;
            set;
        }

        public double WindowSize
        {
            get
            {

                return (1.00 / _sampleRate) * _samplesPerWindow;
            }
        }

        public bool EnableFiltering
        {
            set
            {
                _enableFiltering = value;
            }

            get
            {
                return _enableFiltering;
            }
        }

        public int Divisor
        {
            set
            {
                _spectrogramPane.Divisor = value;
                _spectrogramPane.UpdateGraph();
            }

            get
            {
                return _spectrogramPane.Divisor;
            }
        }

        public int LimitPercent
        {
            get { return _limitPercent; }
            set { _limitPercent = value; }
        }

        public bool isPaused
        {
            get
            {
                return _isPaused;
            }

            set
            {
                _isPaused = value;
            }
        }

        public bool isGreyscale
        {
            get
            {
                return _spectrogramPane.isGreyScale;
            }
            set
            {
                _spectrogramPane.isGreyScale = value;
            }
        }

        public enum WindowTypes
        {
            Hann,
            Hamming,
            Rectangle,
            Gaussian
        }
        public WindowTypes WindowType { get; set; }

        readonly SoundSelectionHandler _selectionHandler;
        public SoundSelectionHandler SelectionHandler
        {
            get
            {
                return _selectionHandler;
            }
        }
        
        private bool _stopSelection = true;

        /// <summary>
        /// Allows the user to select segments
        /// </summary>
        public void StartSelect(string filename)
        {
            SelectionHandler.WavFileName = filename;
            SelectionHandler.LoadSelections();

            if (_stopSelection)
            {
                _stopSelection = false;
                SelectLoop();
            }
        }

        public void StopSelect()
        {
            _stopSelection = true;

            _spectrogramPane.StopSelection();
            _spectrogramPane.SelectedXPoints.Clear();
            _selectIter = 0;
        }

        private int _selectIter = 0;
        private void SelectLoop()
        {
            _spectrogramPane.StartSelection(_selectIter.ToString(), Color.Blue, Color.Red, (object source1, EventArgs e1) =>
            {
                _selectIter++;
                SelectLoop();
            });
        }

        public bool StartRecord()
        {
            try
            {
                _wavSaver = new WAVFile();
                _wavLocation = string.Format(@"{0:yyyy-MM-dd_hh-mm-ss-tt}.wav", DateTime.Now);
                _wavSaver.Create(_wavLocation, false, 44100, 16);
                return true;
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error during record: \n{0}", ex), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                return false;
            }
        }

        public string StopRecord()
        {
            try
            {
                _wavSaver.Close();
                _wavSaver = null;

                SaveFileDialog saveDialog = new SaveFileDialog();
                saveDialog.InitialDirectory = @"C:\Users\mrmt32\Dropbox\MPHYS Project\Breath Sounds\wav files\";
                saveDialog.Filter = "Wave Files (*.WAV)|*.WAV";
                saveDialog.FileName = string.Format(@"{0:yyyy-MM-dd_hh-mm-ss-tt}.wav", DateTime.Now);

                if (saveDialog.ShowDialog() == true)
                {
                    WAVFile inFile = new WAVFile();
                    inFile.Open(_wavLocation, WAVFile.WAVFileMode.READ);

                    WAVFile outFile = new WAVFile();
                    outFile.Create(saveDialog.FileName, false, 44100, 16);


                    while (inFile.NumSamplesRemaining > 0)
                    {
                        short[] sample = new short[] { inFile.GetNextSample_16bit() };

                        outFile.AddSample_16bit(sample[0]);
                    }

                    outFile.Close();
                    inFile.Close();

                    return saveDialog.FileName;
                }
                else
                {
                    return null;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error during record: \n{0}", ex), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }

            return null;
        }

        public void StartLive()
        {
            StopSelect();
            if (_waveInStream != null)
            {
                Stop();
            }

            if (_wavOpener != null)
            {
                _wavOpener.Close();
            }

            if (_wavSaver != null)
            {
                _wavSaver.Close();
            }


            Reset();
            Start();
        }

        public void UnloadWave()
        {
            StopSelect();
            Reset();

            if (_wavOpener != null)
            {
                _wavOpener.Close();
            }
        }

        public bool LoadWave(string fileName, int windowSize = 4096)
        {
            // If we are currently recording from a microphone then stop!
            if (_waveInStream != null)
            {
                Stop();
                Reset();
            }

            if (_wavOpener != null)
            {
                _wavOpener.Close();
            }

            if (_wavSaver != null)
            {
                _wavSaver.Close();
            }

            try
            {
                _wavOpener = new WAVFile();
                _wavOpener.Open(fileName, WAVFile.WAVFileMode.READ);

                //_parentWindow.Title = string.Format("Lung Sounds - {0}", fileName);
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error opening wave file\n{0}", ex), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                return false;
            }

            try
            {
                //_spectrogramPane.Dock = DockStyle.Left;

                _spectrogramPane.WindowSamples = windowSize;

                int sampleId = 0;
                double[] buffer = new double[windowSize];
                while (true)
                {
                    if (sampleId == windowSize)
                    {
                        FFTResult result = PerformFFT(buffer);
                        _spectrogramPane.Windows.Add(result.FFT);
                        sampleId = 0;
                    }

                    if (_wavOpener.FilePosition < _wavOpener.FileSizeBytes)
                    {
                        buffer[sampleId] = (double)_wavOpener.GetNextSampleAs16Bit();
                        sampleId++;
                    }
                    else
                    {
                        break;
                    }
                }

                _spectrogramPane.UpdateGraph();
                StartSelect(fileName);
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error reading wave file\n{0}", ex), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                return false;
            }

            return true;
        }

        public void CombineWaves(string[] inputPaths, string outputPath)
        {
            WAVFile outputWav = new WAVFile();
            outputWav.Create(outputPath, false, 44100, 16);

            FileStream outSel = File.Create(outputPath.ToUpper().Replace(".WAV", ".SPSEL"));
            using (BinaryWriter selectionsWriter = new BinaryWriter(outSel))
            {
                int fileOffset = 0;
                int noiseIdOffset = 0;
                ObservableCollection<SpectrogramPane.XSelection> selections = new ObservableCollection<SpectrogramPane.XSelection>();
                Dictionary<SpectrogramPane.XSelection, int> noiseSelections = new Dictionary<SpectrogramPane.XSelection, int>();

                foreach (string inputPath in inputPaths)
                {
                    WAVFile inputWav = null;
                    int numSamples = 0;
                    try
                    {
                        inputWav = new WAVFile();
                        inputWav.Open(inputPath, WAVFile.WAVFileMode.READ);

                        while (inputWav.NumSamplesRemaining != 0)
                        {
                            outputWav.AddSample_16bit(inputWav.GetNextSample_16bit());
                        }

                        numSamples = inputWav.NumSamples;
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(string.Format("Error opening wave file\n{0}", ex), "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    }
                    finally
                    {
                        if (inputWav != null)
                        {
                            inputWav.Close();
                            inputWav = null;
                        }
                    }

                    // Now sort selections
                    if (File.Exists(inputPath.ToLower().Replace(".wav", ".spsel")))
                    {
                        using (BinaryReader selectionsReader = new BinaryReader(File.Open(inputPath.ToLower().Replace(".wav", ".spsel"), FileMode.Open)))
                        {
                            if (new string(selectionsReader.ReadChars(7)) == "PHSPECT")
                            {
                                selectionsReader.ReadChar();
                                string verString = new string(selectionsReader.ReadChars(4));

                                if (verString == "V102")
                                {
                                    int numSelections = selectionsReader.ReadInt32();

                                    for (int i = 0; i < numSelections; i++)
                                    {
                                        selections.Add(SelectionsWindow.LoadSelection(selectionsReader, null, true, 
                                            noiseSelections, fileOffset, noiseIdOffset));
                                    }

                                    noiseIdOffset += numSelections;
                                }
                            }
                        }
                    }
                    fileOffset += numSamples / 4096 + 1;
                }

                SelectionsWindow.loadNoiseSels(selections, noiseSelections, selections);

                selectionsWriter.Write(new char[] { 'P', 'H', 'S', 'P', 
                                                    'E', 'C', 'T', '\0', 
                                                    'V', '1', '0', '2' });

                selectionsWriter.Write(selections.Count);

                SelectionsWindow.WriteSelections(selections, selectionsWriter);
            }

            outputWav.Close();
        }

        public AudioProcessor(SpectrogramPane spectrogramPane)
        {
            this.isSpectrogramLog = true;
            this.WindowType = WindowTypes.Hann;

            _sampleRate = 44100;
            _bitsPerSample = 16;
            _samplesPerWindow = 4096;
            _bufferSizeMultiplier = 1;

            _spectrogramPane = spectrogramPane;
            _spectrogramPane.WindowSamples = _samplesPerWindow;

            _selectionHandler = new SoundSelectionHandler(_spectrogramPane, _spectrogramPane.WindowSize);
        }
        
        public void Close()
        {
        }

        /// <summary>
        /// Starts capturing and processing sound
        /// </summary>
        public void Start(int samplesPerWindow = 4096)
        {
            try
            {
                _sampleRate = 44100;
                _bitsPerSample = 16;
                _samplesPerWindow = samplesPerWindow;
                _bufferSizeMultiplier = 1;

                // Start recording at sample rate 44100 with 16 bits per sample
                // DataArrive gets run every sample
                int bytesPerSample = _bitsPerSample / 8;

                _waveInStream = new WaveIn();
                _waveInStream.WaveFormat = new NAudio.Wave.WaveFormat(_sampleRate, _bitsPerSample, 1);
                _waveInStream.NumberOfBuffers = 3;
                _waveInStream.BufferBytes = _samplesPerWindow * bytesPerSample * _bufferSizeMultiplier;

                _waveInStream.DataAvailable += waveInStream_DataAvailable;
                _waveInStream.StartRecording();

                _waveOutStream = new WaveOut();
                _waveOutStream.Init(new NAudio.Wave.WaveInProvider(_waveInStream));
                _waveOutStream.Play();

                _spectrogramPane.WindowSamples = _samplesPerWindow;
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("Error while starting recording: {0}", ex));
            }
        }

        void waveInStream_DataAvailable(object sender, WaveInEventArgs e)
        {
            DataArrived(e.Buffer, e.Buffer.Length); //, e.BytesRecorded);
        }

        /// <summary>
        /// Gets called by the WaveOutPlayer each time the output buffer needs to be filled
        /// </summary>
        /// <param name="data">Input buffer data (array of samples of size _bitsPerSample)</param>
        /// <param name="size">Size of the buffer</param>
        private void BufferFill(IntPtr data, int size)
        {

            if (_playerBuffer == null || _playerBuffer.Length < size)
                _playerBuffer = new byte[size];
            if (_playerStream.Length >= size)
                _playerStream.Read(_playerBuffer, 0, size);
            else
                for (int i = 0; i < _playerBuffer.Length; i++)
                    _playerBuffer[i] = 0;
            System.Runtime.InteropServices.Marshal.Copy(_playerBuffer, 0, data, size);

        }

        /// <summary>
        /// Stops all sound processing and capture
        /// </summary>
        public void Stop(bool stopPlayer = true)
        {

            if (stopPlayer && _waveOutStream != null)
            {
                _waveOutStream.Stop();
                _waveOutStream.Dispose();
                _waveOutStream = null;
            }

            if (_waveInStream != null)
            {
                _waveInStream.StopRecording();

                _waveInStream = null;
            }

        }

        /// <summary>
        /// Clears the spectrogram
        /// </summary>
        public void Reset()
        {
            _spectrogramPane.Clear();
        }

        /// <summary>
        /// Called by WaveInRecorder each time it's internal buffer is filled
        /// </summary>
        /// <param name="data">Data contained in the buffer (array of samples of size _bitsPerSample)</param>
        /// <param name="size">Size of the buffer</param>
        private void DataArrived(byte[] data, int size)
        {
            if (_recorderBuffer == null || _recorderBuffer.Length != size)
                _recorderBuffer = new byte[size];

            if (_waveBuffer == null || _waveBuffer.Length != size)
                _waveBuffer = new short[size / (_bitsPerSample / 8)];

            Buffer.BlockCopy(data, 0, _waveBuffer, 0, data.Length);

            // Save wav frame if recording enabled
            if (_wavSaver != null)
            {
                foreach (double sample in _waveBuffer)
                {
                    _wavSaver.AddSample_16bit((short)sample);
                }
            }

            if (_lastCaptureTime != null)
            {
                int msSinceLastCapture = (int)(DateTime.Now - _lastCaptureTime).TotalMilliseconds;
                if (msSinceLastCapture > WindowSize * _bufferSizeMultiplier * 1000 + 30)
                {
                    _lostTime += (int)(msSinceLastCapture - WindowSize * _bufferSizeMultiplier * 1000 + 30);
                }
            }

            _lastCaptureTime = DateTime.Now;

            for (int i = 0; i < _bufferSizeMultiplier; i++)
            {
                DataArrived(i);
            }
        }

        private void DataArrived(int bufferWindowId)
        {
            int startId = _samplesPerWindow * bufferWindowId;

            // Get double array from byte array
            double[] wave = new double[(_recorderBuffer.Length / 2) / _bufferSizeMultiplier];

            Array.Copy(_waveBuffer, startId, wave, 0, wave.Length);

            for (int i = 0; i < wave.Length; i += 1)
            {
                if (Math.Abs(wave[i]) > _maxAmplitude) _maxAmplitude = Math.Abs(wave[i]);
                wave[i] *= _amplification;
            }


            // If graphs are not paused, update them
            FFTResult result = PerformFFT(wave);

            if (!_isPaused && bufferWindowId == _bufferSizeMultiplier - 1)
            {
                UpdateGraphs(wave, result.FFT, true);

                _spectrogramPane.Windows.Add(result.FFT);
            }
            else
            {
                //  spectrogramPane1.Windows.Add(result.FFT);
            }
        }

        /// <summary>
        /// Updates the graphs
        /// </summary>
        /// <param name="wave"></param>
        /// <param name="fft"></param>
        /// <param name="skipSpect"></param>
        /// <param name="startTime"></param>
        private void UpdateGraphs(double[] wave, double[] fft, bool skipSpect = false, double startTime = 0)
        {
            if (!skipSpect) _spectrogramPane.Windows.Add(fft);
        }

        public struct FFTResult
        {
            public double MaxAmplitude { get; set; }
            public double MaxFrequency { get; set; }
            public double[] FFT { get; set; }
        }

        /// <summary>
        /// Perfoms an FFT on the wave form
        /// </summary>
        /// <param name="wave"></param>
        /// <returns></returns>
        public FFTResult PerformFFT(double[] wave)
        {
            AForge.Math.Complex[] fftComp = new AForge.Math.Complex[wave.Length];

            for (int i = 0; i < wave.Length; i++)
            {
                fftComp[i].Re = wave[i] / Math.Pow(2, _bitsPerSample - 1);
            }

            switch (this.WindowType)
            {
                case WindowTypes.Hamming:
                    fftComp = Hamming(fftComp);
                    break;

                case WindowTypes.Hann:
                    fftComp = Hann(fftComp);
                    break;

                case WindowTypes.Gaussian:
                    fftComp = Gaussian(fftComp);
                    break;
            }

            double[] waveProcessed = new double[fftComp.Length];
            for (int i = 0; i < wave.Length; i++)
            {
                waveProcessed[i] = fftComp[i].Re;
            }

            AForge.Math.FourierTransform.FFT(fftComp, AForge.Math.FourierTransform.Direction.Forward);

            double[] fft = new double[fftComp.Length / 2];


            double maxMag = double.NegativeInfinity;
            double minMag = double.PositiveInfinity;

            for (int i = 0; i < fftComp.Length; i++)
            {


                // If we windowed, we need to fix amplitude
                switch (this.WindowType)
                {
                    case WindowTypes.Hamming:
                    case WindowTypes.Hann:
                    case WindowTypes.Gaussian:
                        fftComp[i] = fftComp[i] * 2;
                        break;
                }

                if (fftComp[i].Magnitude > maxMag)
                {
                    maxMag = fftComp[i].Magnitude;
                }

                if (fftComp[i].Magnitude < minMag)
                {
                    minMag = fftComp[i].Magnitude;
                }

                // Trim quiet sounds
                if (fftComp[i].Magnitude < maxMag * ((100 - (double)_limitPercent) / 100))
                {
                    fftComp[i] = new AForge.Math.Complex(0, 0);
                }
            }

            double maxDb = double.NegativeInfinity;
            double maxFreq = double.NegativeInfinity;
            for (int i = 0; i < fftComp.Length / 2; i++)
            {
                double freq = i * (1.00 / WindowSize);

                if (this.isSpectrogramLog)
                {
                    fft[i] = 20.0 * Math.Log10(fftComp[i].Magnitude);
                }
                else
                {
                    fft[i] = fftComp[i].Magnitude;
                }

                if (fft[i] > maxDb)
                {
                    maxDb = fft[i];
                    maxFreq = i * (1.00 / WindowSize);
                }
            }

            FFTResult result = new FFTResult();

            result.FFT = fft;
            result.MaxAmplitude = maxDb;
            result.MaxFrequency = maxFreq;

            return result;
        }

        /// <summary>
        /// Reads in wave file and generates spectrogram
        /// </summary>
        private void SpectrogramFromWav()
        {
            int sampleId = 0;
            double[] buffer = new double[4096];
            while (_wavOpener.FilePosition < _wavOpener.FileSizeBytes)
            {
                if (sampleId == 4096)
                {
                    FFTResult result = PerformFFT(buffer);
                    _spectrogramPane.Windows.Add(result.FFT);
                    sampleId = 0;
                }

                buffer[sampleId] = (double)_wavOpener.GetNextSampleAs16Bit();
                sampleId++;
            }

            _spectrogramPane.UpdateGraph();
        }

        public AForge.Math.Complex[] Hamming(AForge.Math.Complex[] iwv)
        {
            int N = iwv.Length;

            for (int n = 0; n < N; n++)
                iwv[n].Re = iwv[n].Re * (0.54d - 0.46d * (float)Math.Cos((2d * Math.PI * n) / (N - 1)));

            return iwv;
        }

        public AForge.Math.Complex[] Hann(AForge.Math.Complex[] iwv)
        {
            int N = iwv.Length;

            for (int n = 0; n < N; n++)
            {
                iwv[n].Re = iwv[n].Re * 0.5 * (1d - Math.Cos(2d * Math.PI * n / (N - 1)));
            }

            return iwv;
        }

        public AForge.Math.Complex[] Gaussian(AForge.Math.Complex[] iwv)
        {
            int N = iwv.Length;

            for (int n = 0; n < N; n++)
                iwv[n].Re = iwv[n].Re * Math.Exp(-0.5 * Math.Pow((n - (N - 1) / 2) / (0.4 * (N - 1) / 2), 2));

            return iwv;
        }
    }
}
